﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Globalization;
using System.Diagnostics;

namespace InfFront.InfoViews.Base
{
    /// <summary>
    /// Use this type to tell the Settings Engine to not escape/decorate the text when it's stored to the Xml or any other database
    /// </summary>
    public struct RawString
    {
        public string Value { get; set; }
    }

    ////////////////////////////////////////////////////////////////////////////////////
    /// Settings xml database
    ////////////////////////////////////////////////////////////////////////////////////

    public interface ISettingsDbNode
    {
        ISettingsDbNode OpenChildNode(string childName, bool createMissing);

        bool TryGetValue(string fieldName, out object value, out DateTime modTime);
        void SetValue(string fieldName, object value);
        void DeleteValue(string fieldName);
    }

    internal class SettingsDbNode : ISettingsDbNode
    {
        #region Private Variables
        private XmlElement element;
        private static readonly Dictionary<string, IConverterRecord> typeConverters;
        #endregion Private Variables

        #region Construction/Initialization

        static SettingsDbNode()
        {
            typeConverters = new Dictionary<string, IConverterRecord>();

            RegisterConverter(typeof(String), new StringConverter());
            RegisterConverter(typeof(Int16), new Int16Converter());
            RegisterConverter(typeof(Int32), new Int32Converter());
            RegisterConverter(typeof(Int64), new Int64Converter());
            RegisterConverter(typeof(Double), new DoubleConverter());
            RegisterConverter(typeof(Boolean), new BooleanConverter());
            RegisterConverter(typeof(DateTime), new DateTimeConverter());
            typeConverters.Add(typeof(XmlElement).Name, new XmlElementConverterRecord());
            typeConverters.Add(typeof(RawString).Name, new RawStringConverterRecord());
        }

        public SettingsDbNode(XmlElement element)
        {
            this.element = element;
        }

        #endregion Construction/Initialization

        #region Convertors

        private interface IConverterRecord
        {
            Type Type { get; }

            object ConvertFrom(XmlElement elem);
            void ConvertTo(XmlElement elem, object value);
        }

        private struct SimpleConverterRecord: IConverterRecord
        {
            private TypeConverter converter;
            private Type type;

            public SimpleConverterRecord(System.Type type, TypeConverter conv)
            {
                this.type = type;
                this.converter = conv;
            }

            public System.Type Type { get { return type; } }

            public object ConvertFrom(XmlElement elem)
            {
                return converter.ConvertFromString(elem.InnerText);
            }

            public void ConvertTo(XmlElement elem, object value)
            {
                elem.InnerText = converter.ConvertToString(value);
            }
        }

        private class RawStringConverterRecord : IConverterRecord
        {
            public RawStringConverterRecord()
            {
            }

            public System.Type Type { get { return typeof(RawString); } }

            public object ConvertFrom(XmlElement elem)
            {
                return new RawString() { Value = elem.InnerXml };
            }

            public void ConvertTo(XmlElement elem, object value)
            {
                var raws = (RawString)value;
                elem.InnerXml = raws.Value;
            }
        }

        private struct XmlElementConverterRecord : IConverterRecord
        {
            #region IConverterRecord Members

            public Type Type { get { return typeof(XmlElement); } }

            public object ConvertFrom(XmlElement elem)
            {
                return elem;
            }

            public void ConvertTo(XmlElement elem, object value)
            {
                if (Type.ReferenceEquals(elem, value))
                    return;
                var other = (XmlElement)value;
                elem.InnerXml = string.Empty;
                foreach (XmlNode child in other.ChildNodes)
                {
                    if (child is XmlAttribute)
                        continue;
                    elem.AppendChild(child.CloneNode(true));
                }
            }

            #endregion
        }

        private static void RegisterConverter(Type type, TypeConverter converter)
        {
            var rec = new SimpleConverterRecord(type, converter);
            typeConverters.Add(type.Name, rec);
        }

        #endregion Convertors

        #region ISettingsDbNode Members

        public ISettingsDbNode OpenChildNode(string childName, bool createMissing)
        {
            var child = (XmlElement)element.SelectSingleNode(string.Format("SettingNode[@Key='{0}']", childName));
            if (child == null)
            {
                if (!createMissing)
                    return null;
                child = element.OwnerDocument.CreateElement("SettingNode");
                var keyAttr = child.OwnerDocument.CreateAttribute("Key");
                keyAttr.Value = childName;
                child.Attributes.Append(keyAttr);
                element.AppendChild(child);
            }

            return new SettingsDbNode(child);
        }

        public bool TryGetValue(string fieldName, out object value, out DateTime modTime)
        {
            modTime = DateTime.MinValue;
            var child = (XmlElement)element.SelectSingleNode(fieldName);
            if (child == null)
            {
                value = null;
                return false;
            }

            var timeAttr = (XmlAttribute)child.SelectSingleNode("@Modified");
            if (timeAttr != null)
            {
                try
                {
                    modTime = DateTime.Parse(timeAttr.Value, DateTimeFormatInfo.InvariantInfo).ToLocalTime();
                }
                catch (FormatException)
                {
                    modTime = DateTime.Parse(timeAttr.Value);
                }
            }

            var typeAttr = (XmlAttribute)child.SelectSingleNode("@Type");
            if (typeAttr == null)
            {
                value = child.Value;
            }
            else
            {
                if (!typeAttr.Value.Contains("[]"))
                {
                    value = typeConverters[typeAttr.Value].ConvertFrom(child);
                }
                else
                {
                    var nodes = child.SelectNodes("Item");
                    var itemTypeName = typeAttr.Value.TrimEnd('[', ']');
                    var converter = typeConverters[itemTypeName];
                    var array = Array.CreateInstance(converter.Type, nodes.Count);
                    for (int i = 0; i < nodes.Count; ++i)
                        array.SetValue(converter.ConvertFrom((XmlElement)nodes[i]), i);
                    value = array;
                }
            }

            //var accTimeAttr = (XmlAttribute)child.SelectSingleNode("@LastTimeAccessed");
            //if (accTimeAttr == null)
            //{
            //    accTimeAttr = child.OwnerDocument.CreateAttribute("LastTimeAccessed");
            //    child.Attributes.Append(accTimeAttr);
            //}
            //accTimeAttr.Value = DateTime.Now.ToString();

            return true;
        }

        public void SetValue(string fieldName, object value)
        {
            var child = (XmlElement)element.SelectSingleNode(fieldName);
            if (child != null)
                child.RemoveAll();
            else
            {
                child = element.OwnerDocument.CreateElement(fieldName);
                element.AppendChild(child);
            }
            var valType = value.GetType();
            var typeAttr = child.OwnerDocument.CreateAttribute("Type");
            child.Attributes.Append(typeAttr);
            typeAttr.Value = valType.Name;

            if (!valType.IsArray)
            {
                typeConverters[typeAttr.Value].ConvertTo(child, value);
            }
            else
            {
                var converter = typeConverters[typeAttr.Value.TrimEnd('[', ']')];
                var values = (object[])value;
                foreach (var val in values)
                {
                    var itemNode = child.OwnerDocument.CreateElement("Item");
                    child.AppendChild(itemNode);
                    converter.ConvertTo(itemNode, val);
                }
            }

            var modAttr = child.OwnerDocument.CreateAttribute("Modified");
            child.Attributes.Append(modAttr);
            modAttr.Value = DateTime.Now.ToUniversalTime().ToString(DateTimeFormatInfo.InvariantInfo);
        }

        public void DeleteValue(string fieldName)
        {
            var child = (XmlElement)element.SelectSingleNode(fieldName);
            if (child != null)
                element.RemoveChild(child);
        }

        #endregion
    }

    // TODO: support work with two databases here: the user defined and the predefined settings (the last must be read-only)

    public class SettingsDb
    {
        #region Private Variables
        XmlDocument document;
        SettingsDbNode root;
        #endregion Private Variables

        public SettingsDb()
        {
            document = new XmlDocument();
        }

        public ISettingsDbNode Root
        {
            get { return root; }
        }

        public bool LoadFrom(string filePath)
        {
            XmlElement rootElem = null;
            try
            {
                document.Load(filePath);
                rootElem = document.DocumentElement;
            }
            catch (DirectoryNotFoundException) { }
            catch (FileNotFoundException) { }

            if (rootElem == null)
                return false;

            var verAttr = (XmlAttribute)rootElem.SelectSingleNode("@Ver");
            if (verAttr.Value != "1.0")
                throw new NotSupportedException(string.Format("Settings file version '{0}' is not supported", verAttr.Value));

            root = new SettingsDbNode(rootElem);
            return true;
        }

        public void SetEmpty()
        {
            document.LoadXml("<?xml version='1.0'?>" +
                             "<Settings Ver='1.0'/>");
            root = new SettingsDbNode(document.DocumentElement);
        }

        public void SaveTo(string filePath)
        {
            var savedoc = (XmlDocument) document.Clone();
            RecursiveShapeElements(savedoc.DocumentElement);

            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            savedoc.Save(filePath);
        }

        private static bool RecursiveShapeElements(XmlElement root)
        {
            foreach (XmlElement node in root.SelectNodes("SettingNode"))
            {
                if (!RecursiveShapeElements(node))
                    root.RemoveChild(node);
            }
            return root.ChildNodes.Count > 0;
        }

        public static string DefaultAppDataPath
        {
            get
            {
                var path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                path = Path.Combine(path, Assembly.GetEntryAssembly().GetName().Name);
                path = Path.Combine(path, "ViewSettings.xml");
                return path;
            }
        }

        public static string DefaultCommonPath
        {
            get
            {
                var aname = Assembly.GetEntryAssembly().GetName();
                var path = aname.CodeBase.Replace("file:///", string.Empty); // to make it work on the Mono 2.6
                path = Path.GetDirectoryName(path);
                path = Path.Combine(path, "ViewSettings.xml");
                return path;
            }
        }
    }
}
